/*
 *  Copyright (C) 2010 Interactive Media Management
 * 
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 * 
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 * 
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package dk.i2m.converge.ejb.facades;

import dk.i2m.converge.core.search.SearchEngineIndexingException;
import dk.i2m.converge.core.search.IndexingFactory;
import dk.i2m.converge.domain.search.IndexField;
import dk.i2m.converge.domain.search.SearchResult;
import dk.i2m.converge.core.content.MediaItem;
import dk.i2m.converge.core.content.MediaItemStatus;
import dk.i2m.converge.core.content.NewsItem;
import dk.i2m.converge.core.content.NewsItemActor;
import dk.i2m.converge.core.metadata.Concept;
import dk.i2m.converge.core.metadata.GeoArea;
import dk.i2m.converge.core.metadata.Organisation;
import dk.i2m.converge.core.metadata.Person;
import dk.i2m.converge.core.metadata.PointOfInterest;
import dk.i2m.converge.core.metadata.Subject;
import dk.i2m.converge.domain.search.SearchFacet;
import dk.i2m.converge.domain.search.SearchResults;
import dk.i2m.converge.core.security.UserRole;
import dk.i2m.converge.core.ConfigurationKey;
import dk.i2m.converge.ejb.services.ConfigurationServiceLocal;
import java.io.IOException;
import java.net.MalformedURLException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.ejb.EJB;
import javax.ejb.Stateless;
import org.apache.commons.lang.StringUtils;
import org.apache.solr.client.solrj.SolrQuery;
import org.apache.solr.client.solrj.SolrServer;
import org.apache.solr.client.solrj.SolrServerException;
import org.apache.solr.client.solrj.response.FacetField;
import org.apache.solr.client.solrj.response.QueryResponse;
import org.apache.solr.common.SolrDocument;
import org.apache.solr.common.SolrDocumentList;
import org.apache.solr.common.SolrInputDocument;

/**
 * Stateless session bean implementing a search engine service.
 *
 * @author Allan Lykke Christensen
 */
@Stateless
public class SearchEngineBean implements SearchEngineLocal {

    private static final Logger logger = Logger.getLogger(SearchEngineBean.class.getName());

    @EJB private ConfigurationServiceLocal cfgService;

    @EJB private UserFacadeLocal userFacade;

    /** {@inheritDoc} */
    @Override
    public SearchResults search(String query, int start, int rows, String... filterQueries) {
        long startTime = System.currentTimeMillis();
        SearchResults searchResults = new SearchResults();
        try {
            SolrQuery solrQuery = new SolrQuery();
            solrQuery.setStart(start);
            solrQuery.setRows(rows);
            solrQuery.setQuery(query);
            solrQuery.setFacet(true);

            solrQuery.addFacetField(IndexField.TYPE.getName());
            solrQuery.addFacetField(IndexField.OUTLET.getName());
            solrQuery.addFacetField(IndexField.SECTION.getName());
            solrQuery.addFacetField(IndexField.SUBJECT.getName());
            solrQuery.addFacetField(IndexField.ORGANISATION.getName());
            solrQuery.addFacetField(IndexField.PERSON.getName());
            solrQuery.addFacetField(IndexField.LOCATION.getName());
            solrQuery.addFacetField(IndexField.POINT_OF_INTEREST.getName());

            //solrQuery.addFacetField("actor");

            for (UserRole userRole : userFacade.getUserRoles()) {
                solrQuery.addFacetField(userRole.getName());
            }

            solrQuery.addFilterQuery(filterQueries);
            solrQuery.setFacetMinCount(1);
            solrQuery.setIncludeScore(true);
            solrQuery.setHighlight(true).setHighlightSnippets(1); //set other params as needed
            solrQuery.setParam("hl.fl", "title,story,caption");
            solrQuery.setParam("hl.fragsize", "500");
            solrQuery.setParam("hl.simple.pre", "<span class=\"searchHighlight\">");
            solrQuery.setParam("hl.simple.post", "</span>");
            SolrServer srv = getSolrServer();
            QueryResponse qr = srv.query(solrQuery);
            SolrDocumentList sdl = qr.getResults();
            searchResults.setNumberOfResults(sdl.getNumFound());

            for (SolrDocument d : sdl) {

                // Copy all fields to map for easy access
                HashMap<String, Object> values = new HashMap<String, Object>();

                for (Iterator<Map.Entry<String, Object>> i = d.iterator(); i.hasNext();) {
                    Map.Entry<String, Object> e2 = i.next();
                    values.put(e2.getKey(), e2.getValue());
                }

                String type = (String) values.get("type");

                SearchResult hit = null;
                if ("Story".equalsIgnoreCase(type)) {
                    hit = generateStoryHit(qr, values);
                } else if ("Media".equalsIgnoreCase(type)) {
                    hit = generateMediaHit(qr, values);
                }

                if (hit != null) {
                    searchResults.getHits().add(hit);
                }
            }

            List<FacetField> facets = qr.getFacetFields();

            for (FacetField facet : facets) {
                List<FacetField.Count> facetEntries = facet.getValues();
                if (facetEntries != null) {
                    for (FacetField.Count fcount : facetEntries) {
                        if (!searchResults.getFacets().containsKey(facet.getName())) {
                            searchResults.getFacets().put(facet.getName(), new ArrayList<SearchFacet>());
                        }

                        SearchFacet sf = new SearchFacet(fcount.getName(), fcount.getAsFilterQuery(), fcount.getCount());

                        // Check if the filter query is already active
                        for (String fq : filterQueries) {
                            if (fq.equals(fcount.getAsFilterQuery())) {
                                sf.setSelected(true);
                            }
                        }

                        // Ensure that the facet is not already there
                        if (!searchResults.getFacets().get(facet.getName()).contains(sf)) {
                            searchResults.getFacets().get(facet.getName()).add(sf);
                        }
                    }
                }
            }
        } catch (SolrServerException ex) {
            logger.log(Level.SEVERE, null, ex);
        }
        long endTime = System.currentTimeMillis();

        searchResults.setSearchTime(endTime - startTime);
        searchResults.setStart(start);
        searchResults.setResultsPerPage(rows);

        return searchResults;
    }

    /** {@inheritDoc} */
    @Override
    public void index(NewsItem ni) throws SearchEngineIndexingException {
        SolrInputDocument solrDoc = new SolrInputDocument();
        solrDoc.addField(IndexField.ID.getName(), ni.getId(), 1.0f);
        solrDoc.addField(IndexField.TITLE.getName(), ni.getTitle(), 1.0f);
        solrDoc.addField(IndexField.TYPE.getName(), "Story");
        solrDoc.addField(IndexField.BYLINE.getName(), ni.getByLine());
        solrDoc.addField(IndexField.BRIEF.getName(), ni.getBrief());
        solrDoc.addField(IndexField.STORY.getName(), dk.i2m.commons.StringUtils.stripHtml(ni.getStory()));

        if (ni.getEdition() != null) {
            if (ni.getEdition().getPublicationDate() != null) {
                solrDoc.addField(IndexField.DATE.getName(), ni.getEdition().getPublicationDate().getTime());
            }
            solrDoc.addField(IndexField.EDITION_NUMBER.getName(), ni.getEdition().getNumber());
            solrDoc.addField(IndexField.EDITION_VOLUME.getName(), ni.getEdition().getVolume());
        }
        if (ni.getSection() != null) {
            solrDoc.addField(IndexField.SECTION.getName(), ni.getSection().getFullName());
        }
        if (ni.getOutlet() != null) {
            solrDoc.addField(IndexField.OUTLET.getName(), ni.getOutlet().getTitle());
        }
        solrDoc.addField(IndexField.WORD_COUNT.getName(), ni.getWordCount());

        //                for (WorkflowStateTransition wst : ni.getHistory()) {
        //                    doc.add(new Field(IndexField.ACTOR_UID.getName(), wst.getUser().getUsername(), Field.Store.YES, Field.Index.ANALYZED));
        //                    doc.add(new Field(IndexField.ACTOR_NAME.getName(), wst.getUser().getFullName(), Field.Store.YES, Field.Index.ANALYZED));
        //                }


        for (NewsItemActor actor : ni.getActors()) {
            solrDoc.addField(IndexField.ACTOR.getName(), actor.getUser().getFullName());
            // Dynamic fields for the actors role
            solrDoc.addField(actor.getRole().getName(), actor.getUser().getFullName());
        }

        for (Concept concept : ni.getConcepts()) {
            if (concept instanceof Subject) {
                solrDoc.addField(IndexField.SUBJECT.getName(), concept.getFullTitle());
            }
            if (concept instanceof Person) {
                solrDoc.addField(IndexField.PERSON.getName(), concept.getFullTitle());
            }

            if (concept instanceof Organisation) {
                solrDoc.addField(IndexField.ORGANISATION.getName(), concept.getFullTitle());
            }

            if (concept instanceof GeoArea) {
                solrDoc.addField(IndexField.LOCATION.getName(), concept.getFullTitle());
            }

            if (concept instanceof PointOfInterest) {
                solrDoc.addField(IndexField.POINT_OF_INTEREST.getName(), concept.getFullTitle());
            }

            solrDoc.addField(IndexField.CONCEPT.getName(), concept.getFullTitle());
        }
        try {
            getSolrServer().add(solrDoc);
            getSolrServer().commit();
        } catch (SolrServerException ex) {
            throw new SearchEngineIndexingException(ex);
        } catch (IOException ex) {
            throw new SearchEngineIndexingException(ex);
        }
    }

    /** {@inheritDoc} */
    @Override
    public void index(MediaItem mi) throws SearchEngineIndexingException {
        if (mi.getStatus() == null || !mi.getStatus().equals(MediaItemStatus.APPROVED)) {
            logger.log(Level.FINE, "Removing MediaItem #{0} from index", mi.getId());
            try {
                // Delete from index
                getSolrServer().deleteById(String.valueOf(mi.getId()));
                return;
            } catch (Exception ex) {
                throw new SearchEngineIndexingException("Could not remove MediaItem from SearchEngineIndex. " + ex.getMessage());
            }
        }
        logger.log(Level.FINE, "Adding MediaItem #{0} to index", mi.getId());

        SolrInputDocument solrDoc = new SolrInputDocument();
        solrDoc.addField(IndexField.ID.getName(), mi.getId(), 1.0f);
        solrDoc.addField(IndexField.TYPE.getName(), "Media");

        String mediaFormat;
        if (mi.getContentType().startsWith("audio")) {
            mediaFormat = "Audio";
        } else if (mi.getContentType().startsWith("video")) {
            mediaFormat = "Video";
        } else if (mi.getContentType().startsWith("image")) {
            mediaFormat = "Image";
        } else {
            mediaFormat = "Unknown";
        }

        solrDoc.addField(IndexField.MEDIA_FORMAT.getName(), mediaFormat);

        solrDoc.addField(IndexField.TITLE.getName(), mi.getTitle(), 1.0f);
        solrDoc.addField(IndexField.BYLINE.getName(), mi.getByLine());
        solrDoc.addField(IndexField.CAPTION.getName(), dk.i2m.commons.StringUtils.stripHtml(mi.getDescription()));
        solrDoc.addField(IndexField.CONTENT_TYPE.getName(), mi.getContentType());
        solrDoc.addField(IndexField.REPOSITORY.getName(), mi.getMediaRepository().getName());
        if (mi.getMediaDate() != null) {
            solrDoc.addField(IndexField.DATE.getName(), mi.getMediaDate().getTime());
        }

        if (mi.isThumbAvailable()) {
            solrDoc.addField(IndexField.THUMB_URL.getName(), mi.getThumbURL());
        }

        solrDoc.addField(IndexField.ACTOR.getName(), mi.getOwner().getFullName());

        for (Concept concept : mi.getConcepts()) {
            if (concept instanceof Subject) {
                solrDoc.addField(IndexField.SUBJECT.getName(), concept.getFullTitle());
            }
            if (concept instanceof Person) {
                solrDoc.addField(IndexField.PERSON.getName(), concept.getFullTitle());
            }

            if (concept instanceof Organisation) {
                solrDoc.addField(IndexField.ORGANISATION.getName(), concept.getFullTitle());
            }

            if (concept instanceof GeoArea) {
                solrDoc.addField(IndexField.LOCATION.getName(), concept.getFullTitle());
            }

            if (concept instanceof PointOfInterest) {
                solrDoc.addField(IndexField.POINT_OF_INTEREST.getName(), concept.getFullTitle());
            }

            solrDoc.addField(IndexField.CONCEPT.getName(), concept.getFullTitle());
        }

        try {
            getSolrServer().add(solrDoc);
            getSolrServer().commit();
        } catch (SolrServerException ex) {
            throw new SearchEngineIndexingException(ex);
        } catch (IOException ex) {
            throw new SearchEngineIndexingException(ex);
        }
    }

    /** {@inheritDoc} */
    @Override
    public void removeFromIndex(NewsItem item) throws SearchEngineIndexingException {
        try {
            getSolrServer().deleteById(String.valueOf(item.getId()));
        } catch (SolrServerException ex) {
            throw new SearchEngineIndexingException(ex);
        } catch (IOException ex) {
            throw new SearchEngineIndexingException(ex);
        }
    }

    /** {@inheritDoc} */
    @Override
    public void removeFromIndex(MediaItem item) throws SearchEngineIndexingException {
        try {
            getSolrServer().deleteById(String.valueOf(item.getId()));
        } catch (SolrServerException ex) {
            throw new SearchEngineIndexingException(ex);
        } catch (IOException ex) {
            throw new SearchEngineIndexingException(ex);
        }
    }

    /**
     * Generates a {link SearchResult} for a media item.
     *
     * @param qr
     *          QueryResponse from Solr
     * @param values
     *          Fields available
     * @return {@link SearchResult}
     */
    private SearchResult generateMediaHit(QueryResponse qr, HashMap<String, Object> values) {
        String id = (String) values.get(IndexField.ID.getName());

        StringBuilder caption = new StringBuilder();
        StringBuilder title = new StringBuilder();
        StringBuilder note = new StringBuilder();

        Map<String, List<String>> highlighting = qr.getHighlighting().get(id);

        boolean highlightingExist = highlighting != null;

        if (highlightingExist && highlighting.get(IndexField.CAPTION.getName()) != null) {
            for (String hl : highlighting.get(IndexField.CAPTION.getName())) {
                caption.append(hl);
            }
        } else {
            caption.append(StringUtils.abbreviate((String) values.get(IndexField.CAPTION.getName()), 500));
        }

        if (highlightingExist && highlighting.get(IndexField.TITLE.getName()) != null) {
            for (String hl : qr.getHighlighting().get(id).get(IndexField.TITLE.getName())) {
                title.append(hl);
            }
        } else {
            title.append((String) values.get(IndexField.TITLE.getName()));
        }

        note.append((String) values.get(IndexField.TYPE.getName()));
        note.append(" - ");
        note.append((String) values.get(IndexField.MEDIA_FORMAT.getName()));
        note.append(" - ");
        note.append((String) values.get(IndexField.REPOSITORY.getName()));

        SearchResult hit = new SearchResult();
        hit.setId(Long.valueOf(id));
        hit.setTitle(title.toString());
        hit.setDescription(caption.toString());
        hit.setNote(note.toString());
        hit.setLink("{0}/MediaItemArchive.xhtml?id=" + values.get(IndexField.ID.getName()));
        hit.setType((String) values.get(IndexField.TYPE.getName()));

        if (values.containsKey(IndexField.THUMB_URL.getName())) {
            hit.setPreview(true);
            hit.setPreviewLink((String) values.get(IndexField.THUMB_URL.getName()));
        } else {
            hit.setPreview(false);
        }

        if (values.containsKey(IndexField.DATE.getName())) {
            Calendar date = Calendar.getInstance();
            date.setTime((Date) values.get(IndexField.DATE.getName()));
            hit.setDate(date);
        }

        return hit;
    }

    /**
     * Generates a {link SearchResult} for a story.
     *
     * @param qr
     *          QueryResponse from Solr
     * @param values
     *          Fields available
     * @return {@link SearchResult}
     */
    private SearchResult generateStoryHit(QueryResponse qr, HashMap<String, Object> values) {
        String id = (String) values.get(IndexField.ID.getName());

        StringBuilder story = new StringBuilder();
        StringBuilder title = new StringBuilder();
        StringBuilder note = new StringBuilder();

        Map<String, List<String>> highlighting = qr.getHighlighting().get(id);

        boolean highlightingExist = highlighting != null;

        if (highlightingExist && highlighting.get(IndexField.STORY.getName()) != null) {
            for (String hl : highlighting.get(IndexField.STORY.getName())) {
                story.append(hl);
            }
        } else {
            story.append(StringUtils.abbreviate((String) values.get(IndexField.STORY.getName()), 500));
        }

        if (highlightingExist && highlighting.get(IndexField.TITLE.getName()) != null) {
            for (String hl : qr.getHighlighting().get(id).get(IndexField.TITLE.getName())) {
                title.append(hl);
            }
        } else {
            title.append((String) values.get(IndexField.TITLE.getName()));
        }

        note.append((String) values.get(IndexField.TYPE.getName()));
        note.append(" - ");
        note.append((String) values.get(IndexField.OUTLET.getName()));
        note.append(" - Volume: ");

        if (values.containsKey(IndexField.EDITION_VOLUME.getName())) {
            note.append((Integer) values.get(IndexField.EDITION_VOLUME.getName()));
        } else {
            note.append("Unknown");
        }
        note.append(" - Number: ");

        if (values.containsKey(IndexField.EDITION_NUMBER.getName())) {
            note.append((Integer) values.get(IndexField.EDITION_NUMBER.getName()));
        } else {
            note.append("Unknown");
        }

        note.append(" - Section: ");

        if (values.containsKey(IndexField.SECTION.getName())) {
            note.append((String) values.get(IndexField.SECTION.getName()));
        } else {
            note.append("Unknown");
        }

        note.append(" - Words: ");

        if (values.containsKey(IndexField.WORD_COUNT.getName())) {
            note.append(String.valueOf(values.get(IndexField.WORD_COUNT.getName())));
        } else {
            note.append("Unknown");
        }

        SearchResult hit = new SearchResult();
        hit.setId(Long.valueOf(id));
        hit.setTitle(title.toString());
        hit.setDescription(story.toString());
        hit.setNote(note.toString());
        hit.setLink("{0}/NewsItemArchive.xhtml?id=" + id);
        hit.setType((String) values.get(IndexField.TYPE.getName()));

        if (values.containsKey(IndexField.DATE.getName())) {
            Calendar date = Calendar.getInstance();
            date.setTime((Date) values.get(IndexField.DATE.getName()));
            hit.setDate(date);
        }

        return hit;
    }

    /**
     * Gets the instance of the Apache Solr server used for indexing.
     *
     * @return Instance of the Apache Solr server
     * @throws IllegalStateException
     *          If the search engine is not properly configured
     */
    private SolrServer getSolrServer() {
        try {
            return IndexingFactory.getInstance().getSolrServer(cfgService.getString(ConfigurationKey.SEARCH_ENGINE_URL));
        } catch (MalformedURLException ex) {
            logger.log(Level.SEVERE, "Invalid search engine configuration. {0}", ex.getMessage());
            logger.log(Level.FINE, "", ex);
            throw new IllegalStateException("Invalid search engine configuration", ex);
        }
    }
}
